Repository navigation
Conversation
Add a nullable, uniquely indexed uuid column to tables_row_sleeves. Every sleeve insertion, including legacy transfers with explicit IDs, assigns a UUIDv7 before writing. An assigned identity is normalized to lowercase and cannot change or be cleared. Stored values load unchanged, so rows created before this release keep NULL until they are backfilled. User migration archives now carry each row's UUID. Import preserves an unused UUID, generates one when the archive has none, and creates the row with a fresh UUID when the identity is already taken. A unique violation is retried only after a plain read confirms the UUID collision, so other constraint failures surface unchanged. Invalid archived identities fail before anything is written. Refs: nextcloud#3093 Assisted-by: Codex:gpt-6 Assisted-by: Codex:gpt-6-astra Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jiajun Xu <foolfitz@gmail.com>
Register a periodic job from the migration that assigns UUIDv7 values to sleeves still holding NULL. A run processes batches of up to 500 rows for about five seconds and commits each batch. The update only touches rows whose uuid is still NULL, so concurrent assignments and deletions are respected and audit metadata stays unchanged. Parallel runs are disabled. Progress is a lazy app-config cursor written once per run. A lagging cursor only repeats a cheap NULL scan, while writing it after every batch would invalidate the shared app-config cache. When a pass finds nothing left, the job rescans from the start for rows below the cursor before it removes itself. On SQLite the job pauses briefly between batches so requests can take the single writer. Refs: nextcloud#3093 Assisted-by: Codex:gpt-6 Assisted-by: Codex:gpt-6-astra Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jiajun Xu <foolfitz@gmail.com>
Row responses now include the persisted uuid next to the integer id, including public-share rows and the row returned by POST. Rows created before the backfill reaches them report null. Row responses forwarded from a federated server keep the remote uuid, or report null when the remote server does not send one. Regenerate openapi.json and the TypeScript types for the new field. Refs: nextcloud#3093 Assisted-by: Codex:gpt-6 Assisted-by: Codex:gpt-6-astra Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jiajun Xu <foolfitz@gmail.com>
Add a RowIdentifier value object for positive integer IDs within the signed 32-bit range of the id column and for UUIDv7 strings. The row service resolves it inside the requested table or view, evaluating view filters with a bounded single-row query and the same user placeholders as listings, so knowing an identifier grants no access to other rows. Rows that are missing, deleted, in another table or outside a view are now reported as not found instead of as internal errors, including a row removed between loading and updating it. A spreadsheet import counts such a row as one failed line and continues. View projections work on a copy instead of shortening a shared row object, and listings of nodes without columns still return no rows. Row updates accept a RowDataInput so that callers can validate cells first. Refs: nextcloud#3093 Assisted-by: Codex:gpt-6 Assisted-by: Codex:gpt-6-astra Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jiajun Xu <foolfitz@gmail.com>
Add GET /api/2/{nodeCollection}/{nodeId}/rows/{rowId} and let the
existing PUT and DELETE routes accept either a positive numeric ID or a
UUIDv7. Invalid identifiers and malformed cell lists return 400 before
any lookup, and rows outside the requested table or view return 404.
PUT and DELETE resolve only the row ID before mutating, and service
failures are no longer reported as client errors.
Federated nodes keep numeric PUT and DELETE forwarding. UUID addressing
and the new GET return 400 there, because the federation protocol has
no equivalent yet. The existing row routes stay without a rate limit,
since the web UI saves every cell and deletes each selected row through
them.
Regenerate openapi.json and the TypeScript types.
Refs: nextcloud#3093
Assisted-by: Codex:gpt-6
Assisted-by: Codex:gpt-6-astra
Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Jiajun Xu <foolfitz@gmail.com>
foolfitz
marked this pull request as ready for review
November 6, 2026 07:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Related issue: #3093
Add a persistent UUIDv7 to each row so that it can still be identified after export and import through user migration, even if its integer ID changes. The existing integer primary keys and cell associations remain unchanged.
Main changes
tables_row_sleevesand add a unique index covering all rows across the instance. Once assigned, a UUID cannot be replaced or cleared.null, and rows remain accessible by numeric ID./api/2/{tables|views}/{nodeId}/rows/{rowId}in API v2, and allow GET, PUT, and DELETE to accept either an integer ID or a UUID. UUID lookups retain the existing permissions, view filters, and column restrictions. Malformed identifiers return 400; rows not found within the authorized scope return 404. Adduuidto row responses and update the OpenAPI and TypeScript definitions together.The UUID API provided by this PR can read, update, or delete rows on the instance receiving the request. For example, after importing data from Site A into Site B, UUID requests sent to Site B operate only on Site B's data and are not forwarded to Site A.
This defines the scope of this PR. Work left for later includes federated UUID forwarding, UUID addressing on public share token routes, cross-site synchronization, and full
NOT NULLenforcement. UUID preservation applies to user migration archives; CSV import does not yet support migrating rows while retaining their source UUIDs.Regarding the database migration: before merging, confirm with the maintainers which Tables release will include this change and align the migration's version identifier accordingly. This ensures that upgrading an existing installation automatically adds the UUID column and index and schedules background backfill for existing rows.
Testing
Manual testing: preserving UUIDs after archive migration
Using version
0e70ed8c, data with two text columns, Item and Status, was migrated through a User migration archive between two independent Nextcloud 33.0.8 / SQLite environments, with Site A as the source and Site B as the destination.1. Create data on Site A and record the original ID and UUID
A row containing
Transfer notebook/keepwas created, and its identifiers were queried. The request returned HTTP 200, integer ID 4, and UUID01a1ab40-6328-75f1-8976-3cd3e2a98dd8.2. Export a user migration archive from Site A
The
user:export --types=tablescommand exported the Tables data, and the terminal reported a successful export. A separate inspection of the archive confirmed that it preserved the row's UUID and original content.3. Import the same archive into Site B
The
user:importcommand imported the archive into a dedicated empty account on Site B. The terminal reported that the Tables import completed, with the data written to Site B's own database.4. Query the source row on Site A using the original UUID
A GET request using the original UUID was sent from Site A's browser session. It returned HTTP 200, with integer ID 4 and the same UUID as before export.
5. Query the imported row on Site B using the same UUID
A GET request using the same UUID was sent from a separate browser session on Site B. It returned HTTP 200. The integer ID had been reassigned to 108, while the UUID still matched Site A's and the content remained
Transfer notebook/keep. This confirmed that the original UUID could still locate the data after migration.6. Update the content on Site B using the UUID
A PUT request using the UUID path changed Item to
Notebook after transfer. It returned HTTP 200, with integer ID 108, an unchanged UUID, and Status still set tokeep. The table in the screenshot had not yet been reloaded; the PUT response confirms the update.7. Query Site B again to confirm that the update was saved
A separate GET request using the UUID returned HTTP 200, with Item set to
Notebook after transferand both the integer ID and UUID unchanged. The reloaded table also displayed the new value.8. Query Site A again to confirm that the source data was unaffected
A final GET request to Site A using the original UUID returned HTTP 200, with integer ID 4, the same UUID, and the original content,
Transfer notebook/keep. This confirmed that Site B's update had not synchronized back to Site A.This manual test covers archive migration and local UUID GET/PUT operations. It does not cover DELETE, UUID collisions, historical backfill, view-sharing permissions, or federation operations.
Automated verification and upgrade measurements
Local verification records show that the full PHPUnit suite, MySQL/MariaDB/PostgreSQL database tests, Psalm, coding standards, PHP syntax checks, and TypeScript checks all passed. Regenerating the OpenAPI specification and types produced no differences.
An earlier SQLite benchmark with 1,000,014 rows recorded approximately 1.44 seconds for the schema/index migration and 41.48 seconds of total background worker time. All 165 sampled HTTP requests during backfill returned 200. This measurement predates the final cursor-write adjustment and has not been repeated; total worker time does not represent elapsed completion time under the normal cron schedule.
🏁 Checklist
/backport to stableX.X🤖 AI (if applicable)
Codex assisted with developing the code and tests, and Claude Code assisted with their review. I performed the manual migration test and captured the screenshots; Codex assisted with drafting the PR description.